/* Copyright 2008. All rights reserved. */
package org.bling.swt;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.bling.swt.widgets.Break;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;

/**
 * <code>LayoutHelper</code> exists to extract the persisted state
 * and logic out of the <code>BlingLayout</code> object.  This helper
 * is where the heavy lifting to determine if a wrap to the next line
 * should occer, etc.
 * 
 * @author DISBROB
 */
public class BlingLayoutHelper {

	private static final Logger LOG = LogManager
			.getLogger(BlingLayoutHelper.class);

	/*
	 * The current X,Y coordinates
	 */
	private Point currentLocation = new Point(0, 0);

	/*
	 * The Layout Data for the composite we are laying out. This data contains
	 * important rendering information, such as margins.
	 */
	private BlingLayoutData layoutData = null;

	/*
	 * List of all controls visible in this composite container
	 */
	List<Control> controls = new ArrayList<Control>();

	/*
	 * When controls are added to the layout helper, they first come here. This
	 * array is a working storage, in which controls are 'staged' prior to being
	 * placed in the 'controls' array. This helps in determining componentBounds
	 */
	List<Control> working = new ArrayList<Control>();

	// The controls on the page currently floating
	private final Map<Rectangle, Integer> floats = new HashMap<Rectangle, Integer>();

	/*
	 * This number is indicative of how much width is taken up on the layout by
	 * floating elements.
	 */
	private int cachedFloatOffsetX = 0;

	/*
	 * This variable relates to the maximum bounds [width, height] to contain
	 * the controls added to the layout.
	 */
	private Point compositeBounds = null;

	/*
	 * The size of the composite in question. REVIEW: How is this different from
	 * compositeBounds? compositeBounds is calculated, compositeSize is passed
	 * in.
	 */
	private Point compositeSize = null;

	int originX = 0;

	/*
	 * Default constructor
	 */
	public BlingLayoutHelper(Point compositeSize, Point currentLocation,
			BlingLayoutData layoutData) {
		this.currentLocation = currentLocation;
		originX = currentLocation.x;
		this.layoutData = layoutData;
		this.compositeSize = compositeSize;

		// Initialize the variables to accommodate for the margins
		cachedFloatOffsetX = layoutData.marginRight;
		this.currentLocation.x += layoutData.marginRight;

		compositeBounds = new Point(layoutData.marginLeft
				+ layoutData.marginRight, layoutData.marginTop
				+ layoutData.marginBottom);
	}

	private int getCurrentUsableWidth() {
		if (compositeSize.x <= 0) {
			return Display.getDefault().getClientArea().width
					- layoutData.marginLeft - cachedFloatOffsetX;
		}

		return compositeSize.x - layoutData.marginLeft - cachedFloatOffsetX;
	}

	/*
	 * Add a control to the layout
	 */
	public void add(Control control, Point childSize, BlingLayoutData data) {

		LOG.debug("Adding control to the layout");
		if (data.isFloat()) {
			// FIXME: This only handles float:right
			Rectangle floatRect = new Rectangle(compositeSize.x
					- getFloatOffsetX() - childSize.x, currentLocation.y,
					childSize.x, childSize.y);
			control.setBounds(floatRect);
			floats.put(control.getBounds(), Integer.valueOf(currentLocation.y));
			cachedFloatOffsetX += control.getBounds().x;
		} else {
			Rectangle rect = new Rectangle(currentLocation.x,
					currentLocation.y, childSize.x, childSize.y);

			control.setBounds(rect);

			boolean shouldLineBreak = false;
			if (data.isLineBreak()) {
				// Hard line break found
				shouldLineBreak = true;
			} else if (!working.isEmpty()) {
				// If there are controls in the working queue, then perform a
				// line break;

				// If adding this control exceeds the current usable width of
				// this line, perform a line break
				// do not take into account the originX or left margin
				if ((currentLocation.x - originX - layoutData.marginLeft + childSize.x) > getCurrentUsableWidth()) {
					shouldLineBreak = true;
				}
			}

			if (shouldLineBreak) {
				// We must first break before we add the control to the working
				// queue, since the lineBreak() method will use the working
				// queue to place the widgets onto the layout.
				lineBreak();
				if (!(control instanceof Break)) {
					working.add(control);
					control.setBounds(currentLocation.x, currentLocation.y,
							childSize.x, childSize.y);
					currentLocation.x += childSize.x;
				}
			} else {
				if (!data.isFloat()) {
					working.add(control);
					control.setBounds(currentLocation.x, currentLocation.y,
							childSize.x, childSize.y);
					currentLocation.x += control.getSize().x;
				}
			}
		}

	}

	public Point getCompositeBounds() {
		Point workingStats = calculateWorkingLineHeightAndWidth();

		// If calculated bounds are smaller than suggested bounds, return the
		// maximum of either bounds
		int maxWidth = Math.max(compositeBounds.x, compositeSize.x);
		maxWidth = Math.max(maxWidth, workingStats.x);
		int maxHeight = Math.max(compositeBounds.y + workingStats.y, compositeSize.y);

		return new Point(maxWidth, maxHeight);
	}

	private Point calculateWorkingLineHeightAndWidth() {
		int workingControlTotalWidth = layoutData.marginLeft;
		int maxHeight = 0;
		Iterator<Control> workingIterator = working.iterator();
		while (workingIterator.hasNext()) {
			Control control = (Control) workingIterator.next();
			maxHeight = Math.max(maxHeight, control.getSize().y);
			workingControlTotalWidth += control.getSize().x;
		}
		workingControlTotalWidth += layoutData.marginRight;

		return new Point(workingControlTotalWidth, maxHeight);
	}

	private void lineBreak() {
		Point workingStats = calculateWorkingLineHeightAndWidth();
		addHeight(workingStats.y);
		compositeBounds.x = Math.max(workingStats.x, compositeBounds.x);
		compositeBounds.y += workingStats.y;
		controls.addAll(working);
		working = new ArrayList<Control>();
		currentLocation.x = originX + layoutData.marginLeft;
	}

	/*
	 * When a break, line feed, or overflow has occurred, the layout will call
	 * this method to trigger a break to the next line.
	 */
	private void addHeight(int height) {
		currentLocation.y += height;

		// Process Floats
		Iterator<Map.Entry<Rectangle, Integer>> entries = floats.entrySet()
				.iterator();
		while (entries.hasNext()) {
			Map.Entry<Rectangle, Integer> entry = entries.next();
			Rectangle rect = entry.getKey();
			Integer y = entry.getValue();
			if (rect.y + y.intValue() >= currentLocation.y) {
				floats.remove(entry.getKey());
				cachedFloatOffsetX -= rect.x;
			}
		}

	}

	private int getFloatOffsetX() {
		return cachedFloatOffsetX;
	}

	public List<Control> getAllControls() {
		List<Control> retval = new ArrayList<Control>();
		retval.addAll(controls);
		retval.addAll(working);
		return retval;
	}
}
